﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpiceNet.Circuits;
using SpiceNet.Integration;
using SpiceNet.Diagnostics;
using SpiceNet.Parameters;

namespace SpiceNet.Models.Waveforms
{
    /// <summary>
    /// A waveform that can generate a pulse value
    /// </summary>
    public class Pulse : Waveform
    {
        /// <summary>
        /// Private variables
        /// </summary>
        private double v1, v2, td, tr, tf, pw, per;

        #region Parameters
        private enum _c { PULSE_V1 = 1, PULSE_V2 = 2, PULSE_TD = 3, PULSE_TR = 4, PULSE_TF = 5, PULSE_PW = 6, PULSE_PERIOD = 7 }
        private static Dictionary<string, IP> _pTable = new Dictionary<string, IP>
        {
            { "v1", new IP(IF.IOP, (int)_c.PULSE_V1, T.REAL, "The initial value") },
            { "v2", new IP(IF.IOP, (int)_c.PULSE_V2, T.REAL, "The peak value") },
            { "td", new IP(IF.IOP, (int)_c.PULSE_TD, T.REAL, "The initial delay time in seconds") },
            { "tr", new IP(IF.IOP, (int)_c.PULSE_TR, T.REAL, "The rise time in seconds") },
            { "tf", new IP(IF.IOP, (int)_c.PULSE_TF, T.REAL, "The fall time in seconds") },
            { "pw", new IP(IF.IOP, (int)_c.PULSE_PW, T.REAL, "The pulse width in seconds") },
            { "period", new IP(IF.IOP, (int)_c.PULSE_PERIOD, T.REAL, "The period in seconds") }
        };

        /// <summary>
        /// Get a list of parameters
        /// </summary>
        public override Dictionary<string, IP> ParamTable => _pTable;

        public Parameter<double> V1 { get; } = new Parameter<double>();
        public Parameter<double> V2 { get; } = new Parameter<double>();
        public Parameter<double> Delay { get; } = new Parameter<double>();
        public Parameter<double> RiseTime { get; } = new Parameter<double>();
        public Parameter<double> FallTime { get; } = new Parameter<double>();
        public Parameter<double> PulseWidth { get; } = new Parameter<double>();
        public Parameter<double> Period { get; } = new Parameter<double>();
        #endregion

        /// <summary>
        /// Constructor
        /// </summary>
        public Pulse()
            : base("Pulse")
        {
        }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="v1"></param>
        /// <param name="v2"></param>
        /// <param name="td"></param>
        /// <param name="tr"></param>
        /// <param name="tf"></param>
        /// <param name="pw"></param>
        /// <param name="per"></param>
        public Pulse(double v1, double v2, double td, double tr, double tf, double pw, double per)
            : base("Pulse")
        {
            V1.Par(v1);
            V2.Par(v2);
            Delay.Par(td);
            RiseTime.Par(tr);
            FallTime.Par(tf);
            PulseWidth.Par(pw);
            Period.Par(per);
        }

        /// <summary>
        /// Set parameter
        /// </summary>
        protected override void Param(int id, object value, Circuit ckt = null)
        {
            switch ((_c)id)
            {
                case _c.PULSE_V1: V1.Par((double)value); break;
                case _c.PULSE_V2: V2.Par((double)value); break;
                case _c.PULSE_TD: Delay.Par((double)value); break;
                case _c.PULSE_TR: RiseTime.Par((double)value); break;
                case _c.PULSE_TF: FallTime.Par((double)value); break;
                case _c.PULSE_PW: PulseWidth.Par((double)value); break;
                case _c.PULSE_PERIOD: Period.Par((double)value); break;
                default:
                    throw new BadParameterException();
            }
        }

        /// <summary>
        /// Ask parameter
        /// </summary>
        protected override object Ask(int id, Circuit ckt = null)
        {
            switch ((_c)id)
            {
                case _c.PULSE_V1: return V1.Value;
                case _c.PULSE_V2: return V2.Value;
                case _c.PULSE_TD: return Delay.Value;
                case _c.PULSE_TR: return RiseTime.Value;
                case _c.PULSE_TF: return FallTime.Value;
                case _c.PULSE_PW: return PulseWidth.Value;
                case _c.PULSE_PERIOD: return Period.Value;
                default:
                    throw new BadParameterException();
            }
        }

        /// <summary>
        /// Setup the waveform
        /// </summary>
        /// <param name="instance"></param>
        /// <param name="ckt"></param>
        public override void Setup(Instance instance, Circuit ckt)
        {
            v1 = V1;
            v2 = V2;
            td = Delay;
            tr = RiseTime;
            tf = FallTime;
            pw = PulseWidth;
            per = Period;
        }

        /// <summary>
        /// Calculate the waveform at a specific moment
        /// </summary>
        /// <param name="time"></param>
        /// <returns></returns>
        public override double At(double time)
        {
            double basetime = 0.0;

            // Get a relative time variable
            time -= td;
            if (time > per)
            {
                basetime = per * Math.Floor(time / per);
                time -= basetime;
            }

            if (time <= 0.0 || time >= tr + pw + tf)
                return v1;
            else if (time >= tr && time <= tr + pw)
                return v2;
            else if (time > 0 && time < tr)
                return v1 + (v2 - v1) * time / tr;
            else
                return v2 + (v1 - v2) * (time - tr - pw) / tf;
        }

        /// <summary>
        /// Accept the current time point
        /// </summary>
        /// <param name="ckt"></param>
        public override void Accept(Circuit ckt)
        {
            // Should not be here
            if ((ckt.Mode & (Circuit.Modes.Tran | Circuit.Modes.TranOp)) == 0)
                return;

            IntegrationMethod integ = ckt.Integration;
            Breakpoints breaks = integ.Breaks;

            double time = ckt.State.Time - td;
            double basetime = 0.0;
            if (time >= per)
            {
                basetime = per * Math.Floor(time / per);
                time -= basetime;
            }

            double tol = 1e-7 * pw;
            if (time <= 0 || time >= tr + pw + tf)
            {
                if (integ.Break && Math.Abs(time) < tol)
                    breaks.SetBreakpoint(basetime + tr + td);
                else if (integ.Break && Math.Abs(time - tr - pw - tf) < tol)
                    breaks.SetBreakpoint(basetime + per + td);
                else if (integ.Break && time == -td)
                    breaks.SetBreakpoint(basetime + td);
                else if (integ.Break && Math.Abs(time - per) < tol)
                    breaks.SetBreakpoint(basetime + td + tr + per);
            }
            else if (time >= tr && time <= tr + pw)
            {
                if (integ.Break && Math.Abs(time - tr) < tol)
                    breaks.SetBreakpoint(basetime + td + tr + pw);
                else if (integ.Break && Math.Abs(time - tr - pw) < tol)
                    breaks.SetBreakpoint(basetime + td + tr + pw + tf);
            }
            else if (time > 0 && time < tr)
            {
                if (integ.Break && Math.Abs(time) < tol)
                    breaks.SetBreakpoint(basetime + td + tr);
                else if (integ.Break && Math.Abs(time - tr) < tol)
                    breaks.SetBreakpoint(basetime + td + tr + pw);
            }
            else
            {
                if (integ.Break && Math.Abs(time - tr - pw) < tol)
                    breaks.SetBreakpoint(basetime + td + tr + pw + tf);
                else if (integ.Break && Math.Abs(time - tr - pw - tf) < tol)
                    breaks.SetBreakpoint(basetime + td + per);
            }
        }
    }
}
